/**
 * Handles the Api pane ui state. It looks into the routing based on actions too
 * */
import get from "lodash/get";
import omit from "lodash/omit";
import { all, call, put, select, take, takeEvery } from "redux-saga/effects";
import type {
  ReduxAction,
  ReduxActionWithMeta,
} from "actions/ReduxActionTypes";
import {
  ReduxActionErrorTypes,
  ReduxActionTypes,
  ReduxFormActionTypes,
} from "ee/constants/ReduxActionConstants";
import { getFormData } from "selectors/formSelectors";
import {
  API_EDITOR_FORM_NAME,
  QUERY_EDITOR_FORM_NAME,
} from "ee/constants/forms";
import {
  CONTENT_TYPE_HEADER_KEY,
  EMPTY_KEY_VALUE_PAIRS,
  HTTP_METHOD,
  POST_BODY_FORMAT_OPTIONS,
  POST_BODY_FORMAT_OPTIONS_ARRAY,
} from "PluginActionEditor/constants/CommonApiConstants";
import { DEFAULT_CREATE_API_CONFIG } from "PluginActionEditor/constants/ApiEditorConstants";
import { DEFAULT_CREATE_GRAPHQL_CONFIG } from "PluginActionEditor/constants/GraphQLEditorConstants";
import history from "utils/history";
import { autofill, change, initialize, reset } from "redux-form";
import type { Property } from "api/ActionAPI";
import { getQueryParams } from "utils/URLUtils";
import { getPluginIdOfPackageName } from "sagas/selectors";
import {
  getAction,
  getActionByBaseId,
  getDatasourceActionRouteInfo,
  getPlugin,
} from "ee/selectors/entitiesSelector";
import {
  createActionRequest,
  setActionProperty,
} from "actions/pluginActionActions";
import type {
  Action,
  ApiAction,
  AutoGeneratedHeader,
  CreateApiActionDefaultsParams,
} from "entities/Action";
import { type Plugin, PluginPackageName, PluginType } from "entities/Plugin";
import { getCurrentWorkspaceId } from "ee/selectors/selectedWorkspaceSelectors";
import log from "loglevel";
import type { EventLocation } from "ee/utils/analyticsUtilTypes";
import { createMessage, ERROR_ACTION_RENAME_FAIL } from "ee/constants/messages";
import { parseUrlForQueryParams, queryParamsRegEx } from "utils/ApiPaneUtils";
import { updateReplayEntity } from "actions/pageActions";
import { ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils";
import { apiEditorIdURL, datasourcesEditorIdURL } from "ee/RouteBuilder";
import { getCurrentBasePageId } from "selectors/editorSelectors";
import { validateResponse } from "./ErrorSagas";
import type { CreateDatasourceSuccessAction } from "actions/datasourceActions";
import { removeTempDatasource } from "actions/datasourceActions";
import { deriveAutoGeneratedHeaderState } from "../PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/utils/autoGeneratedHeaders";
import { TEMP_DATASOURCE_ID } from "constants/Datasource";
import type { FeatureFlags } from "ee/entities/FeatureFlag";
import { selectFeatureFlags } from "ee/selectors/featureFlagsSelectors";
import { isGACEnabled } from "ee/utils/planHelpers";
import { getHasManageActionPermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
import {
  getApplicationByIdFromWorkspaces,
  getCurrentApplicationIdForCreateNewApp,
} from "ee/selectors/applicationSelectors";
import { DEFAULT_CREATE_APPSMITH_AI_CONFIG } from "PluginActionEditor/constants/AppsmithAIEditorConstants";
import { checkAndGetPluginFormConfigsSaga } from "./PluginSagas";
import { convertToBasePageIdSelector } from "selectors/pageListSelectors";
import type { ApplicationPayload } from "entities/Application";
import { klonaLiteWithTelemetry } from "utils/helpers";

function* syncApiParamsSaga(
  actionPayload: ReduxActionWithMeta<string, { field: string }>,
  actionId: string,
) {
  const field = actionPayload.meta.field;
  //Payload here contains the path and query params of a typical url like https://{domain}/{path}?{query_params}
  const value = actionPayload.payload;

  // Regular expression to find the query params group
  if (field === "actionConfiguration.path") {
    const params = parseUrlForQueryParams(value);

    // before updating the query parameters make sure the path field changes have been successfully updated first
    yield take(ReduxActionTypes.BATCH_UPDATES_SUCCESS);
    yield put(
      autofill(
        API_EDITOR_FORM_NAME,
        "actionConfiguration.queryParameters",
        params,
      ),
    );
    yield put(
      setActionProperty({
        actionId: actionId,
        propertyName: "actionConfiguration.queryParameters",
        value: params,
      }),
    );
  } else if (field.includes("actionConfiguration.queryParameters")) {
    const { values } = yield select(getFormData, API_EDITOR_FORM_NAME);
    const path = values.actionConfiguration.path || "";
    const matchGroups = path.match(queryParamsRegEx) || [];
    const currentPath = matchGroups[1] || "";
    const paramsString = values.actionConfiguration.queryParameters
      .filter((p: Property) => p.key)
      .map(
        (p: Property, i: number) => `${i === 0 ? "?" : "&"}${p.key}=${p.value}`,
      )
      .join("");

    yield put(
      autofill(
        API_EDITOR_FORM_NAME,
        "actionConfiguration.path",
        `${currentPath}${paramsString}`,
      ),
    );
  }
}

function* handleUpdateBodyContentType(contentType: string) {
  const { values } = yield select(getFormData, API_EDITOR_FORM_NAME);

  const displayFormatValue = POST_BODY_FORMAT_OPTIONS_ARRAY.find(
    (el) => el === contentType,
  );

  if (!displayFormatValue) {
    log.error("Display format not supported", contentType);

    return;
  }

  // get headers
  const headers = klonaLiteWithTelemetry(
    values?.actionConfiguration?.headers,
    "ApiPaneSagas.handleUpdateBodyContentType.headers",
  );

  // set autoGeneratedHeaders
  const autoGeneratedHeaders: AutoGeneratedHeader[] = [];

  // Set an auto generated content type header for all post body format options except none.
  // also if we wish to add more auto genrated content-type the code goes inside here.
  if (displayFormatValue !== POST_BODY_FORMAT_OPTIONS.NONE) {
    // get content type header index
    const contentTypeHeaderIndex = headers.findIndex(
      (element: { key: string; value: string }) =>
        element &&
        element.key &&
        element.key.trim().toLowerCase() === CONTENT_TYPE_HEADER_KEY,
    );

    // if theres content type
    if (contentTypeHeaderIndex !== -1) {
      autoGeneratedHeaders.push({
        key: CONTENT_TYPE_HEADER_KEY,
        value: displayFormatValue,
        isInvalid: true,
      });
    } else {
      autoGeneratedHeaders.push({
        key: CONTENT_TYPE_HEADER_KEY,
        value: displayFormatValue,
        isInvalid: false,
      });

      // Example of setting extra auto generated header.

      // autoGeneratedHeaders.push({
      //   key: "content-length",
      //   value: "0",
      //   isInvalid: false,
      // });
    }
  }

  // change the autoGeneratedHeader value.
  yield put(
    change(
      API_EDITOR_FORM_NAME,
      "actionConfiguration.autoGeneratedHeaders",
      autoGeneratedHeaders,
    ),
  );

  // help to prevent cyclic dependency error in case the bodyFormData is empty.

  const bodyFormData = klonaLiteWithTelemetry(
    values?.actionConfiguration?.bodyFormData,
    "ApiPaneSagas.handleUpdateBodyContentType.bodyFormData",
  );

  if (
    displayFormatValue === POST_BODY_FORMAT_OPTIONS.FORM_URLENCODED ||
    displayFormatValue === POST_BODY_FORMAT_OPTIONS.MULTIPART_FORM_DATA
  ) {
    if (!bodyFormData || bodyFormData?.length === 0) {
      yield put(
        change(
          API_EDITOR_FORM_NAME,
          "actionConfiguration.bodyFormData",
          EMPTY_KEY_VALUE_PAIRS.slice(),
        ),
      );
    }
  }
}

function* changeApiSaga(
  actionPayload: ReduxAction<{
    id: string;
    isSaas: boolean;
    action?: Action;
  }>,
) {
  const { id, isSaas } = actionPayload.payload;
  let { action } = actionPayload.payload;

  if (!action) action = yield select(getAction, id);

  if (!action) return;

  if (isSaas) {
    yield put(initialize(QUERY_EDITOR_FORM_NAME, action));
  } else {
    yield put(initialize(API_EDITOR_FORM_NAME, action));

    if (
      action.actionConfiguration &&
      action.actionConfiguration.queryParameters?.length
    ) {
      // Sync the api params my mocking a change action
      yield call(
        syncApiParamsSaga,
        {
          type: ReduxFormActionTypes.ARRAY_REMOVE,
          payload: action.actionConfiguration.queryParameters,
          meta: {
            field: "actionConfiguration.queryParameters",
          },
        },
        id,
      );
    }
  }

  //Retrieve form data with synced query params to start tracking change history.
  const { values: actionPostProcess } = yield select(
    getFormData,
    API_EDITOR_FORM_NAME,
  );

  yield put(updateReplayEntity(id, actionPostProcess, ENTITY_TYPE.ACTION));
}

function* setApiBodyTabHeaderFormat(apiContentType?: string) {
  let displayFormat;

  if (apiContentType) {
    if (Object.values(POST_BODY_FORMAT_OPTIONS).includes(apiContentType)) {
      displayFormat = apiContentType;
    } else {
      displayFormat = POST_BODY_FORMAT_OPTIONS.RAW;
    }
  } else {
    displayFormat = POST_BODY_FORMAT_OPTIONS.NONE;
  }

  // update the body content type based on content type headers.
  yield put(
    change(
      API_EDITOR_FORM_NAME,
      "actionConfiguration.formData.apiContentType",
      displayFormat,
    ),
  );
}

function* formValueChangeSaga(
  actionPayload: ReduxActionWithMeta<
    string,
    { field: string; form: string; index?: number }
  >,
) {
  try {
    const { field, form } = actionPayload.meta;

    if (form !== API_EDITOR_FORM_NAME) return;

    if (field === "dynamicBindingPathList" || field === "name") return;

    const { values } = yield select(getFormData, API_EDITOR_FORM_NAME);
    const featureFlags: FeatureFlags = yield select(selectFeatureFlags);
    const isFeatureEnabled = isGACEnabled(featureFlags);

    if (!values.id) return;

    if (field === "actionConfiguration.formData.apiContentType") {
      yield call(handleUpdateBodyContentType, actionPayload.payload);
    }

    if (
      !getHasManageActionPermission(isFeatureEnabled, values.userPermissions)
    ) {
      yield validateResponse({
        status: 403,
        resourceType: values?.pluginType,
        resourceId: values.id,
      });
    }

    const contentTypeHeaderIndex =
      values?.actionConfiguration?.headers?.findIndex(
        (header: { key: string; value: string }) =>
          header?.key?.trim().toLowerCase() === CONTENT_TYPE_HEADER_KEY,
      );

    const autoGeneratedContentTypeHeaderIndex =
      values?.actionConfiguration?.autoGeneratedHeaders?.findIndex(
        (header: { key: string; value: string }) =>
          header?.key?.trim().toLowerCase() === CONTENT_TYPE_HEADER_KEY,
      );

    const autoGeneratedHeaders =
      get(values, "actionConfiguration.autoGeneratedHeaders") || [];

    if (
      actionPayload.type === ReduxFormActionTypes.ARRAY_REMOVE ||
      actionPayload.type === ReduxFormActionTypes.ARRAY_PUSH
    ) {
      const value = get(values, field);

      yield put(
        setActionProperty({
          actionId: values.id,
          propertyName: field,
          value,
        }),
      );

      // if the user triggers a delete operation on any headers field
      if (field === `actionConfiguration.headers`) {
        // we get the updated auto generated header state based on the user specified content-type.
        const newAutoGeneratedHeaderState: AutoGeneratedHeader[] =
          deriveAutoGeneratedHeaderState(
            values?.actionConfiguration?.headers,
            autoGeneratedHeaders,
          );

        // update the autogenerated headers with the new autogenerated headers state.
        yield put(
          change(
            API_EDITOR_FORM_NAME,
            "actionConfiguration.autoGeneratedHeaders",
            newAutoGeneratedHeaderState,
          ),
        );
      }
    } else {
      yield put(
        setActionProperty({
          actionId: values.id,
          propertyName: field,
          value: actionPayload.payload,
        }),
      );

      if (field.includes("actionConfiguration.headers")) {
        // if user is changing the header keys and if autoGenerated headers exist, derive new state based on the header value.
        if (
          field.includes(".key") &&
          autoGeneratedHeaders &&
          autoGeneratedHeaders.length > 0
        ) {
          const newAutoGeneratedHeaderState = deriveAutoGeneratedHeaderState(
            values?.actionConfiguration?.headers || [],
            autoGeneratedHeaders,
          );

          yield put(
            change(
              API_EDITOR_FORM_NAME,
              "actionConfiguration.autoGeneratedHeaders",
              newAutoGeneratedHeaderState,
            ),
          );
        }
      }

      // when the httpMethod is changed
      if (field === "actionConfiguration.httpMethod") {
        const value = actionPayload.payload;

        // if the user is switching to any other type of httpMethod apart from GET we add an autogenerated content type.
        if (value !== HTTP_METHOD.GET) {
          // if the autoGenerated header does not have any key-values or if there's no content type in the headers
          // we add a default content-type of application/json and set the body tab appropriately.
          if (
            autoGeneratedHeaders.length < 1 ||
            autoGeneratedContentTypeHeaderIndex === -1
          ) {
            const newAutoGeneratedHeaders: AutoGeneratedHeader[] = [
              ...autoGeneratedHeaders,
            ];

            if (contentTypeHeaderIndex !== -1) {
              newAutoGeneratedHeaders.push({
                key: CONTENT_TYPE_HEADER_KEY,
                value: POST_BODY_FORMAT_OPTIONS.JSON,
                isInvalid: true,
              });
            } else {
              newAutoGeneratedHeaders.push({
                key: CONTENT_TYPE_HEADER_KEY,
                value: POST_BODY_FORMAT_OPTIONS.JSON,
                isInvalid: false,
              });
            }

            // change the autoGeneratedHeader value.
            yield put(
              change(
                API_EDITOR_FORM_NAME,
                "actionConfiguration.autoGeneratedHeaders",
                newAutoGeneratedHeaders,
              ),
            );
            // set the body tab.
            yield call(
              setApiBodyTabHeaderFormat,
              POST_BODY_FORMAT_OPTIONS.JSON,
            );
          }
        }
      }
    }

    yield all([call(syncApiParamsSaga, actionPayload, values.id)]);

    // We need to refetch form values here since syncApuParams saga and updateFormFields directly update reform form values.
    const { values: formValuesPostProcess } = yield select(
      getFormData,
      API_EDITOR_FORM_NAME,
    );

    yield put(
      updateReplayEntity(
        formValuesPostProcess.id,
        formValuesPostProcess,
        ENTITY_TYPE.ACTION,
      ),
    );
  } catch (error) {
    yield put({
      type: ReduxActionErrorTypes.SAVE_PAGE_ERROR,
      payload: {
        error,
      },
    });
    yield put(reset(API_EDITOR_FORM_NAME));
  }
}

function* handleActionCreatedSaga(actionPayload: ReduxAction<Action>) {
  const {
    applicationId,
    baseId: baseActionId,
    pageId,
    pluginType,
  } = actionPayload.payload;
  const action: Action | undefined = yield select(
    getActionByBaseId,
    baseActionId,
  );
  const data = action ? { ...action } : {};

  if (pluginType === PluginType.API) {
    yield put(initialize(API_EDITOR_FORM_NAME, omit(data, "name")));
    let basePageId: string = yield select(convertToBasePageIdSelector, pageId);

    if (!basePageId) {
      const application: ApplicationPayload = yield select(
        getApplicationByIdFromWorkspaces,
        applicationId,
      );

      if (application && Array.isArray(application.pages)) {
        basePageId =
          application.pages.find((page) => page.id === pageId)?.baseId || "";
      }
    }

    history.push(
      apiEditorIdURL({
        basePageId,
        baseApiId: baseActionId,
        params: {
          editName: "true",
          from: "datasources",
        },
      }),
    );
  }
}

export function* handleDatasourceCreatedSaga(
  actionPayload: CreateDatasourceSuccessAction,
) {
  const plugin: Plugin | undefined = yield select(
    getPlugin,
    actionPayload.payload.pluginId,
  );

  // Only look at API plugins
  if (plugin && plugin.type !== PluginType.API) return;

  const currentApplicationIdForCreateNewApp: string | undefined = yield select(
    getCurrentApplicationIdForCreateNewApp,
  );
  const application: ApplicationPayload | undefined = yield select(
    getApplicationByIdFromWorkspaces,
    currentApplicationIdForCreateNewApp || "",
  );

  const actionRouteInfo: ReturnType<typeof getDatasourceActionRouteInfo> =
    yield select(getDatasourceActionRouteInfo);

  // This will ensure that API if saved as datasource, will get attached with datasource
  // once the datasource is saved
  if (
    !!actionRouteInfo.baseApiId &&
    actionPayload.payload?.id !== TEMP_DATASOURCE_ID
  ) {
    const action: Action = yield select(
      getActionByBaseId,
      actionRouteInfo.baseApiId,
    );

    yield put(
      setActionProperty({
        actionId: action?.id,
        propertyName: "datasource",
        value: actionPayload.payload,
      }),
    );

    // we need to wait for action to be updated with respective datasource,
    // before redirecting back to action page, hence added take operator to
    // wait for update action to be complete.
    yield take(ReduxActionTypes.UPDATE_ACTION_SUCCESS);

    yield put({
      type: ReduxActionTypes.STORE_AS_DATASOURCE_COMPLETE,
    });

    // temp datasource data is deleted here, because we need temp data before
    // redirecting to api page, otherwise it will lead to invalid url page
    yield put(removeTempDatasource());
  }

  const { redirect } = actionPayload;

  // redirect back to api page
  if (actionRouteInfo && redirect) {
    history.push(
      apiEditorIdURL({
        baseParentEntityId: actionRouteInfo?.baseParentEntityId ?? "",
        baseApiId: actionRouteInfo.baseApiId ?? "",
      }),
    );
  } else if (
    !currentApplicationIdForCreateNewApp ||
    (!!currentApplicationIdForCreateNewApp &&
      actionPayload.payload.id !== TEMP_DATASOURCE_ID)
  ) {
    const basePageId: string = !!currentApplicationIdForCreateNewApp
      ? application?.defaultBasePageId
      : yield select(getCurrentBasePageId);

    history.push(
      datasourcesEditorIdURL({
        basePageId,
        datasourceId: actionPayload.payload.id,
        params: {
          from: "datasources",
          ...getQueryParams(),
          pluginId: plugin?.id,
        },
      }),
    );
  }
}

export function* createDefaultApiActionPayload(
  props: CreateApiActionDefaultsParams,
) {
  const workspaceId: string = yield select(getCurrentWorkspaceId);
  const { apiType, from, newActionName } = props;
  const pluginId: string = yield select(getPluginIdOfPackageName, apiType);
  // Default Config is Rest Api Plugin Config
  // TODO: Fix this the next time the file is edited
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  let defaultConfig: any = DEFAULT_CREATE_API_CONFIG;
  let pluginType: PluginType = PluginType.API;

  if (apiType === PluginPackageName.GRAPHQL) {
    defaultConfig = DEFAULT_CREATE_GRAPHQL_CONFIG;
  }

  if (apiType === PluginPackageName.APPSMITH_AI) {
    defaultConfig = DEFAULT_CREATE_APPSMITH_AI_CONFIG;
    pluginType = PluginType.AI;
    yield call(checkAndGetPluginFormConfigsSaga, pluginId);
  }

  return {
    actionConfiguration: defaultConfig.config,
    name: newActionName,
    datasource: {
      name: defaultConfig.datasource.name,
      pluginId,
      workspaceId,
      datasourceConfiguration: defaultConfig.datasource.datasourceConfiguration,
    },
    pluginType,
    eventData: {
      actionType: defaultConfig.eventData.actionType,
      from: from,
    },
  };
}

/**
 * Creates an API with datasource as DEFAULT_REST_DATASOURCE (No user created datasource)
 * @param action
 */
function* handleCreateNewApiActionSaga(
  action: ReduxAction<{
    pageId: string;
    from: EventLocation;
    apiType?: string;
  }>,
) {
  const { apiType = PluginPackageName.REST_API, from, pageId } = action.payload;

  if (pageId) {
    // Note: Do NOT send pluginId on top level here.
    // It breaks embedded rest datasource flow.

    const createApiActionPayload: Partial<ApiAction> = yield call(
      createDefaultApiActionPayload,
      {
        apiType,
        from,
      },
    );

    yield put(
      createActionRequest({
        ...createApiActionPayload,
        pageId,
      }), // We don't have recursive partial in typescript for now.
    );
  }
}

function* handleApiNameChangeSaga(
  action: ReduxAction<{ id: string; name: string }>,
) {
  yield put(change(API_EDITOR_FORM_NAME, "name", action.payload.name));
}

function* handleApiNameChangeSuccessSaga(
  action: ReduxAction<{ actionId: string }>,
) {
  const { actionId } = action.payload;
  const actionObj: Action | undefined = yield select(getAction, actionId);

  yield take(ReduxActionTypes.FETCH_ACTIONS_FOR_PAGE_SUCCESS);

  if (!actionObj) {
    yield put({
      type: ReduxActionErrorTypes.SAVE_ACTION_NAME_ERROR,
      payload: {
        actionId,
        show: true,
        error: { message: createMessage(ERROR_ACTION_RENAME_FAIL, "") },
        logToSentry: true,
      },
    });

    return;
  }

  if (actionObj.pluginType === PluginType.API) {
    const params = getQueryParams();

    if (params.editName) {
      params.editName = "false";
    }

    const basePageId: string = yield select(
      convertToBasePageIdSelector,
      actionObj.pageId,
    );

    history.push(
      apiEditorIdURL({
        basePageId,
        baseApiId: actionObj.baseId,
        params,
      }),
    );
  }
}

function* handleApiNameChangeFailureSaga(
  action: ReduxAction<{ oldName: string }>,
) {
  yield put(change(API_EDITOR_FORM_NAME, "name", action.payload.oldName));
}

export default function* root() {
  yield all([
    takeEvery(ReduxActionTypes.API_PANE_CHANGE_API, changeApiSaga),
    takeEvery(ReduxActionTypes.CREATE_ACTION_SUCCESS, handleActionCreatedSaga),
    takeEvery(
      ReduxActionTypes.CREATE_DATASOURCE_SUCCESS,
      handleDatasourceCreatedSaga,
    ),
    takeEvery(ReduxActionTypes.SAVE_ACTION_NAME_INIT, handleApiNameChangeSaga),
    takeEvery(
      ReduxActionTypes.SAVE_ACTION_NAME_SUCCESS,
      handleApiNameChangeSuccessSaga,
    ),
    takeEvery(
      ReduxActionErrorTypes.SAVE_ACTION_NAME_ERROR,
      handleApiNameChangeFailureSaga,
    ),
    takeEvery(
      ReduxActionTypes.CREATE_NEW_API_ACTION,
      handleCreateNewApiActionSaga,
    ),
    takeEvery(ReduxFormActionTypes.VALUE_CHANGE, formValueChangeSaga),
    takeEvery(ReduxFormActionTypes.ARRAY_REMOVE, formValueChangeSaga),
    takeEvery(ReduxFormActionTypes.ARRAY_PUSH, formValueChangeSaga),
  ]);
}
